#!/usr/bin/perl

=head1 NAME

Swignition::DataModel::Graph - A graph of triples.

=cut

package Swignition::DataModel::Graph;

use Carp;
use RDF::Redland;
use Swignition::DataModel::Node;

use constant TRUE      =>  1;
use constant FALSE     =>  0;
use strict;

our @ISA = ("Swignition::DataModel::Node");
our $VERSION = '0.1';

=head1 CONSTRUCTORS

=over

=item $graph = Swignition::DataModel::Graph->new($model)

Create a new graph. 

=cut

sub new
{
	my $class = shift;
	my $model = shift;
	
	my $this = Swignition::DataModel::Node::new(__PACKAGE__, $model);

	# @@TODO : tell model that I exist!

	$this->{'_'}->{'usage'}->{'has_triple'} = [];
	$this->{'_'}->{'usage'}->{'has_node'}   = [];
	$this->{'_'}->{'mint'} = 1;
	$this->{'_'}->{'combined'} = 0;
	$this->{'_'}->{'cached_output'} = {};
	
	$this->{'_'}->{'nodeid_dictionary'} = {};
	
	return $this;
}


=back

=head1 PUBLIC METHODS

C<Swignition::DataModel::Graph> also inherits from C<Swignition::DataModel::Node>, so
it can be treated like a node in its own right.

=over

=item $graph->model

Get the model. 

@@TODO Don't know why this isn't being inherited from ::Node!

=cut

sub model
{
	my $this = shift;
	return $this->{'_'}->{'usage'}->{'defined_in'};
}

=item $graph->size

How many triples are there?

=cut

sub size
{
	my $this = shift;
	return scalar @{ $this->get_triples };
}

=item $graph->get_triples

Get a list of all triples in this graph.

=cut

sub get_triples
{
	my $this = shift;
	return $this->{'_'}->{'usage'}->{'has_triple'};
}

=item $graph->get_nodes

Get a list of all nodes used by triples in this graph.

=cut

sub get_nodes
{
	my $this = shift;
	return $this->{'_'}->{'usage'}->{'has_node'};
}

=item $graph->lookup_node($uri_or_nodeid, $mint_if_not_found)

Return the node known in this graph by the $uri_or_nodeid. If looking up a URI
and not found, then looks it up in the rest of the model. If looking up a
nodeid and not found, then can mint a brand new node (if the second argument
is true).

=cut

sub lookup_node
{
	my $this = shift;
	my $uri  = shift;
	my $mint = shift;
	
	if ($uri =~ /^_:(.+)$/)
	{
		if ($mint && !defined $this->{'_'}->{'nodeid_dictionary'}->{$uri})
		{
			$this->{'_'}->{'nodeid_dictionary'}->{$uri} = Swignition::DataModel::BlankNode->new($this->model);
			$this->{'_'}->{'nodeid_dictionary'}->{$uri}->claim_name($1);
		}
		
		return $this->{'_'}->{'nodeid_dictionary'}->{$uri};
	}
	else
	{
		return $this->model->lookup_node($uri, $mint);
	}
}

=item $graph->add_triple($triple)

Add a triple to this graph.

=cut

sub add_triple
{
	my $this   = shift;
	my $triple = shift;
	my $croak  = shift;
	
	$croak = TRUE unless defined $croak;
	croak "Please do not add triples directly to the combined graph.\n"
		if ($this->{'_'}->{'combined'} && $croak);
	
	# Notify the triple's nodes that they are being added to the graph.
	$triple->subject->add_usage('node_in', $this);
	$triple->predicate->add_usage('node_in', $this);
	$triple->object->add_usage('node_in', $this);
	
	# Notify the triple that it is being added to the graph
	$triple->add_usage('triple_of', $this);
	
	# Bring the triple's nodes into the graph.
	$this->add_usage('has_node', $triple->subject);
	$this->add_usage('has_node', $triple->predicate);
	$this->add_usage('has_node', $triple->object);
	
	# Bring the triple itself into the graph.
	my $rv = $this->add_usage('has_triple', $triple);

	# Add them all to the model.
	$this->model->add_node($triple->subject);
	$this->model->add_node($triple->predicate);
	$this->model->add_node($triple->object);
	$this->model->add_node($triple);
	
	# Triple also needs to be added to the combined graph unless this is already it!
	$this->model->combined_graph->add_triple($triple, FALSE)
		unless ($this->{'_'}->{'combined'});
	
	# A new triple invalidates any cached output models.
	$this->{'_'}->{'cached_output'} = {};

	return $triple;
}

=item $triple = $graph->quick($subject_node, $predicate_node, $object_node)

Shortcut to add a triple to this graph without explicitly creating the triple
first.

=cut

sub quick
{
	my $this   = shift;
	
	my $triple = Swignition::DataModel::Triple->new($this->model, @_);
	return $this->add_triple($triple);
}

=item $triple = $graph->quick_rrr($subject_string, $predicate_string, $object_string)

Shortcut to add a triple to this graph without explicitly creating the triple
first. The subject, predicate and object must each be a string containing
either a URI or a bnode prefixed with '_:'.

=cut

sub quick_rrr
{
	my $this   = shift;
	
	my @t;
	foreach my $i (0..2)
	{
		my $uri = shift;
		
		if ((ref $uri) && $uri->isa('Swignition::DataModel::Node'))
		{
			$t[$i] = $uri;
		}
		else
		{
			croak "quick_rrr must be passed three URIs/nodeids\n"
				unless length $uri;
			$t[$i] = $this->lookup_node($uri, TRUE);
		}
	}
	
	my $triple = Swignition::DataModel::Triple->new($this->model, @t);
	return $this->add_triple($triple);
}

=item $triple = $graph->quick_rrl($subject_string, $predicate_string, $literal, $lang, $datatype, $dom)

Shortcut to add a triple to this graph without explicitly creating the triple
first. The subject, and predicate must each be a string containing either a URI
or a bnode prefixed with '_:'. The object is a literal with optionally a
language, datatype and DOM node object.

=cut

sub quick_rrl
{
	my $this   = shift;
	
	my @t;
	foreach my $i (0..1)
	{
		my $uri = shift;
		
		if ((ref $uri) && $uri->isa('Swignition::DataModel::Node'))
		{
			$t[$i] = $uri;
		}
		else
		{
			croak "quick_rrl must be passed two URIs/nodeids and a string\n"
				unless length $uri;
			$t[$i] = $this->lookup_node($uri, TRUE);
		}
	}
	
	if (defined $_[3])
		{ $t[2] = Swignition::DataModel::XMLLiteral->new($this->model, @_); }
	else
		{ $t[2] = Swignition::DataModel::Literal->new($this->model, @_); }
	
	my $triple = Swignition::DataModel::Triple->new($this->model, @t);
	return $this->add_triple($triple);
}

=item $graph->mergeable($an_other)

Returns true if (theoretically) this node can be merged with another node.

=cut

sub mergeable
{
	return FALSE;
}

=item $boolean = $graph->rdf_compatible

Returns true if this graph is RDF-compatible.

=cut

sub rdf_compatible
{
	my $this = shift;
	
	my $compat = TRUE;
	foreach my $triple (@{$this->get_triples})
	{
		$compat &&= $triple->rdf_compatible;
	}
	
	return $compat;
}

=item $json = $graph->as_json

Despite the name, this does not actually return the graph as JSON, but rather
as a Perl hashref which is suitable for conversion to RDF/JSON.

The keys of the hashref are the subjects of the graph. The values for those
keys are themselves hashrefs where the keys are predicates. The values of these
second level hashrefs are arrayrefs. The items in the arrayrefs are hashrefs,
each of which has keys 'type' (which may be 'uri', 'bnode' or 'literal') and
'value' (which contains the URI, bnode identifier or literal string), with
optional strings 'lang' and 'datatype'. See the RDF/JSON specification for more
information and examples.

Note that Swignition::DataModel::Graph goes beyond the capabilities of RDF.
For example, it can store triples where the subject is a literal and the
predicate is a bnode. These are intentionally omitted from the structure
returned by this method.

=cut

sub as_json
{
	my $this = shift;
	
	return $this->{'_'}->{'cached_output'}->{'json'}
		if defined $this->{'_'}->{'cached_output'}->{'json'};
	
	my $rv = {};
	foreach my $triple (@{$this->get_triples})
	{
		next unless $triple->rdf_compatible;
	
		my $val = { };
		if ($triple->object->is_literal)
		{
			$val->{'value'}    = $triple->object->string;
			$val->{'type'}     = 'literal';
			$val->{'datatype'} = $triple->object->datatype
				if length $triple->object->datatype;
			$val->{'lang'}     = $triple->object->lang
				if length $triple->object->lang;
		}
		elsif ($triple->object->uri)
		{
			$val->{'value'} = $triple->object->uri . '';
			$val->{'type'}  = 'uri';
		}
		else
		{
			$val->{'value'} = $triple->object->identity . '';
			$val->{'type'}  = 'bnode';
		}
		
		push @{ $rv->{ $triple->subject->identity }->{ $triple->predicate->identity } }, $val;
	}
	
	$this->{'_'}->{'cached_output'}->{'json'} = $rv;
	return $rv;
}

=item $redland_model = $graph->as_redland

Returns a Redland model of the current graph. See L<RDF::Redland::Model>.

=cut

sub as_redland
{
	my $this  = shift;
	
	return $this->{'_'}->{'cached_output'}->{'redland'}
		if defined $this->{'_'}->{'cached_output'}->{'redland'};
	
	my $rnode = {};	
	foreach my $node (@{ $this->get_nodes })
	{
		my $rn = undef;
		if ($node->is_literal)
			{ $rn = RDF::Redland::LiteralNode->new($node->string, $node->datatype, $node->lang); }
		elsif ($node->uri)
			{ $rn = RDF::Redland::URINode->new(''.$node->uri); }
		else
			{ $rn = RDF::Redland::BlankNode->new(substr($node->identity, 2)); }
			
		$rnode->{ $node->internal_id } = $rn;
	}
	
	my $storage = RDF::Redland::Storage->new("hashes", "test", "new='yes',hash-type='memory'");
	my $model = RDF::Redland::Model->new($storage, "");

	foreach my $triple (@{ $this->get_triples })
	{
		next unless $triple->rdf_compatible;
		
		$model->add(
			$rnode->{$triple->subject->internal_id},
			$rnode->{$triple->predicate->internal_id},
			$rnode->{$triple->object->internal_id});
	}
	
	$this->{'_'}->{'cached_output'}->{'redland'} = $model;
	return $model;
}

=back

=cut

1;

